// Package stl provides functionality for writing 3D models in STL (stereolithography) binary format.
//
// STL is a widely used file format for 3D printing and computer-aided design (CAD). This package
// implements the binary STL format specification, which is more compact and efficient than the ASCII
// variant. The binary format consists of:
//   - An 80-byte header
//   - A 4-byte unsigned integer indicating the number of triangles
//   - Triangle data, where each triangle consists of:
//   - A normal vector (3 x 32-bit floats)
//   - Three vertices (9 x 32-bit floats)
//   - A 2-byte attribute count (unused in most applications)
//
// This package provides optimized writing capabilities with buffered I/O and efficient memory usage,
// making it suitable for generating large 3D models.
package stl

import (
	"bufio"
	"encoding/binary"
	"math"
	"os"

	"github.com/github/gh-skyline/internal/errors"
	"github.com/github/gh-skyline/internal/logger"
	"github.com/github/gh-skyline/internal/types"
)

const (
	// bufferSize defines the size of the write buffer in bytes (1MB).
	// This value is optimized for balancing memory usage and I/O performance
	// when writing large STL files.
	bufferSize = 1024 * 1024

	// triangleSize represents the size of a single triangle in bytes.
	// Each triangle consists of:
	// - Normal vector: 3 x 4 bytes (float32) = 12 bytes
	// - Three vertices: 9 x 4 bytes (float32) = 36 bytes
	// - Attribute count: 2 bytes
	// Total: 50 bytes
	triangleSize = (12 * 4) + 2

	// maxTriangleCount defines the maximum number of triangles allowed in an STL file.
	maxTriangleCount = uint64(math.MaxUint32)
)

// bufferWriter encapsulates common buffer writing operations
type bufferWriter struct {
	buffer []byte
	offset int
}

// writeFloat32 writes a float32 value to the buffer in little-endian format
func (w *bufferWriter) writeFloat32(value float32) {
	binary.LittleEndian.PutUint32(w.buffer[w.offset:], math.Float32bits(value))
	w.offset += 4
}

// writePoint3D writes a Point3DFloat32 to the buffer
func (w *bufferWriter) writePoint3D(p types.Point3DFloat32) {
	w.writeFloat32(p.X)
	w.writeFloat32(p.Y)
	w.writeFloat32(p.Z)
}

// writeSTLHeader writes the 80-byte header to the STL file.
// The header typically contains version or generator information.
func writeSTLHeader(writer *bufio.Writer) error {
	header := make([]byte, 80)
	copy(header, []byte("Generated by GitHub Contributions Skyline Generator"))
	if _, err := writer.Write(header); err != nil {
		return errors.New(errors.IOError, "failed to write STL header", err)
	}
	return nil
}

// writeTriangleCount writes the 4-byte unsigned integer indicating
// the number of triangles in the STL file.
func writeTriangleCount(writer *bufio.Writer, count uint32) error {
	if err := binary.Write(writer, binary.LittleEndian, count); err != nil {
		return errors.New(errors.IOError, "failed to write triangle count", err)
	}
	return nil
}

// writeTrianglesData writes all triangles to the STL file using a pre-allocated buffer.
// Reports progress every 10000 triangles via the logger.
func writeTrianglesData(writer *bufio.Writer, triangles []types.Triangle) error {
	log := logger.GetLogger()
	triangleBuffer := make([]byte, triangleSize)

	for i, triangle := range triangles {
		if err := writeTriangleToBuffer(triangleBuffer, triangle.ToFloat32()); err != nil {
			return errors.New(errors.IOError, "failed to write triangle", err)
		}

		if _, err := writer.Write(triangleBuffer); err != nil {
			return errors.New(errors.IOError, "failed to write triangle data", err)
		}

		if (i+1)%10000 == 0 {
			if err := log.Debug("Written %d/%d triangles", i+1, len(triangles)); err != nil {
				return errors.New(errors.IOError, "failed to log progress", err)
			}
		}
	}
	return nil
}

// WriteSTLBinary writes triangles to a binary STL file with optimized buffering.
//
// The binary STL format consists of:
// 1. 80-byte header - typically containing version/generator information
// 2. 4-byte unsigned integer - number of triangles
// 3. For each triangle:
//   - Normal vector: 3 x float32 (12 bytes)
//   - Vertex 1: 3 x float32 (12 bytes)
//   - Vertex 2: 3 x float32 (12 bytes)
//   - Vertex 3: 3 x float32 (12 bytes)
//   - Attribute byte count: uint16 (2 bytes, usually 0)
func WriteSTLBinary(filename string, triangles []types.Triangle) error {
	if filename == "" {
		return errors.New(errors.ValidationError, "STL filename cannot be empty", nil)
	}

	file, err := os.Create(filename)
	if err != nil {
		return errors.New(errors.IOError, "failed to create STL file", err)
	}
	defer func() {
		if cerr := file.Close(); cerr != nil {
			err = errors.New(errors.IOError, "failed to close STL file", cerr)
		}
	}()

	writer := bufio.NewWriterSize(file, bufferSize)
	defer func() {
		if ferr := writer.Flush(); ferr != nil {
			err = errors.New(errors.IOError, "failed to flush writer", ferr)
		}
	}()

	if err := writeSTLHeader(writer); err != nil {
		return err
	}

	triangleCount := uint64(len(triangles))
	if triangleCount > maxTriangleCount {
		return errors.New(errors.ValidationError, "triangle count exceeds valid range for STL format", nil)
	}

	// Now safely convert to uint32 since we know it's in range
	triangleCountUint32 := uint32(triangleCount)
	if err := writeTriangleCount(writer, triangleCountUint32); err != nil {
		return err
	}

	if err := writeTrianglesData(writer, triangles); err != nil {
		return err
	}

	return nil
}

// writeTriangleToBuffer writes a triangle using an optimized buffer writer
func writeTriangleToBuffer(buffer []byte, t types.TriangleFloat32) error {
	if len(buffer) < triangleSize {
		return errors.New(errors.ValidationError, "buffer too small for triangle data", nil)
	}

	w := &bufferWriter{buffer: buffer}
	w.writePoint3D(t.Normal)
	w.writePoint3D(t.V1)
	w.writePoint3D(t.V2)
	w.writePoint3D(t.V3)
	binary.LittleEndian.PutUint16(buffer[w.offset:], 0) // attribute count

	return nil
}
